package com.googlecode.mvcguice;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.collections.iterators.IteratorEnumeration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.googlecode.mvcguice.action.Action;
import com.googlecode.mvcguice.action.RedirectAction;
import com.googlecode.mvcguice.action.StringContentAction;
import com.googlecode.mvcguice.action.ViewRenderAction;
import com.googlecode.mvcguice.routing.UrlPattern;
import com.googlecode.mvcguice.view.TemplateServlet;

public final class ControllerDispatcher {
	private static final Logger LOGGER = LoggerFactory.getLogger(ControllerDispatcher.class);

	private static final String TEMPLATE_SERVLET_NAME = "Template";

	private final UrlPattern urlPattern;
	private final Set<Method> methods;
	private final ControllerAction controllerAction;

	public ControllerDispatcher(UrlPattern urlPattern, Set<Method> methods, ControllerAction controllerAction) {
		this.urlPattern = checkNotNull(urlPattern);
		this.methods = checkNotNull(methods);
		this.controllerAction = checkNotNull(controllerAction);
	}

	public boolean dispatch(HttpServletRequest request, HttpServletResponse response, ServletContext servletContext) {
		final Method method = Method.valueOf(request.getMethod());
		final String url = request.getPathInfo();

		final Map<String, String> variables = match(method, url);
		if (variables == null) {
			return false;
		} else {
			final RequestContext requestContext = new RequestContext(request, response, variables);
			LOGGER.debug("Dispatching request '{} {}' with '{}'...", new Object[] { method, url, this });
			final Object result = controllerAction.invoke(requestContext);
			process(requestContext, servletContext, result);
			LOGGER.debug("Dispatched request '{} {}' with '{}'", new Object[] { method, url, this });
			return true;
		}
	}

	@Override
	public String toString() {
		return methods + ":" + urlPattern + " -> " + controllerAction;
	}

	private Map<String, String> match(Method method, String url) {
		if (!methods.contains(method)) {
			return null;
		}
		return urlPattern.match(url);
	}

	private void process(RequestContext requestContext, ServletContext servletContext, Object result) {

		if (result instanceof Action) {
			handleAction(requestContext, servletContext, (Action) result);
		} else {
			LOGGER.warn("Illegal result: '{}'", result);
		}
	}

	private void handleAction(RequestContext requestContext, ServletContext servletContext, Action action) {
		if (action instanceof ViewRenderAction) {
			render(requestContext, servletContext, (ViewRenderAction) action);
		} else if (action instanceof RedirectAction) {
			redirect(requestContext.getResponse(), (RedirectAction) action);
		} else if (action instanceof StringContentAction) {
			handleString(requestContext, servletContext, (StringContentAction) action);
		} else {
			throw new IllegalArgumentException("Unsupported Action: '" + action + "'");
		}
	}

	private void handleString(RequestContext requestContext, ServletContext servletContext, StringContentAction action) {
		final HttpServletResponse response = requestContext.getResponse();
		response.setCharacterEncoding("UTF-8");
		response.setContentType("text/html");
		try {
			response.getWriter().write(action.getContent());
			response.flushBuffer();
		} catch (IOException e) {
			// TODO: refactore it
			throw new RuntimeException(e);
		}
	}

	private void render(RequestContext requestContext, ServletContext servletContext, ViewRenderAction action) {
		RequestDispatcher requestDispatcher = servletContext.getNamedDispatcher(TEMPLATE_SERVLET_NAME);

		if (requestDispatcher == null) {
			throw new RuntimeException("Cannot resolve request dispatcher");
		}

		final HttpServletRequest request = requestContext.getRequest();

		//TODO: move it to config
		final String pathInfo = TemplateServlet.TEMPLATE_DIR + controllerAction.getActionPath() + ".html";
		for (Entry<String, Object> e : action.getAttributes().entrySet()) {
			request.setAttribute(e.getKey(), e.getValue());
		}
		final ViewHttpServletRequest viewRequest =
				new ViewHttpServletRequest(request, pathInfo, requestContext.getVariables());
		try {
			requestDispatcher.forward(viewRequest, requestContext.getResponse());
		} catch (ServletException e) {
			//FIXME: add correct exception handling
			throw new RuntimeException(e);
		} catch (IOException e) {
			//FIXME: add correct exception handling
			throw new RuntimeException(e);
		}
	}

	private void redirect(HttpServletResponse response, RedirectAction action) {
		try {
			response.sendRedirect(action.getLocation());
		} catch (IOException e) {
			//FIXME: add correct exception handling
			throw new RuntimeException(e);
		}
	}

	public static class ViewHttpServletRequest extends HttpServletRequestWrapper {
		private final String pathInfo;
		private final Map<String, String[]> parameters;

		public ViewHttpServletRequest(HttpServletRequest request, String pathInfo, Map<String, String> vars) {
			super(request);
			this.pathInfo = pathInfo;

			@SuppressWarnings("unchecked")
			final Map<String, String[]> origParams = request.getParameterMap();
			final Map<String, String[]> tmpParams = new HashMap<String, String[]>(origParams.size() + vars.size());
			tmpParams.putAll(origParams);
			for (Entry<String, String> e : vars.entrySet()) {
				tmpParams.put(e.getKey(), new String[] { e.getValue() });
			}

			parameters = tmpParams;
		}

		@Override
		public String getPathInfo() {
			return pathInfo;
		}

		@Override
		public Map<String, String[]> getParameterMap() {
			return parameters;
		}

		@SuppressWarnings("unchecked")
		@Override
		public Enumeration<String> getParameterNames() {
			return new IteratorEnumeration(parameters.keySet().iterator());
		}

		@Override
		public String getParameter(String name) {
			final String[] values = parameters.get(name);
			return values == null ? null : values[0];
		}

		@Override
		public String[] getParameterValues(String name) {
			return parameters.get(name);
		}
	}
}
